reduce函数

1
2
3
4
5
                  _                       
_ __ ___ __| | _ _ ___ ___
| '__| / _ \ / _` | | | | | / __| / _ \
| | | __/ | (_| | | |_| | | (__ | __/
|_| \___| \__,_| \__,_| \___| \___|

reduce是stream的收集器的一种,对于我来说理解起来比较难,我抽出一天半时间来理解.
找到了三个对于理解reduce帮助很大的文章:


reduce有三个重载方法: 一个参数,二个参数,三个参数

三个参数reduce:

首先理解方法本身的意思:
Stream的reduce方法,翻译过来是聚合或者是汇聚成一个的意思,
由于Stream本身就代表着一堆数据,那stream.reduce()方法顾名思义就是把一堆数据聚合成一个数据.

理解了reduce方法的意思,再来看看这个方法挂靠的对象是stream,是一个流,了解一下流的工作方式:
流底层核心其实是Spliterator接口的一个实现,而这个Spliterator接口其实本身就是Fork/Join并行框架的一个实现,
所以归根结底要明白流的工作方式,就要明白一下Fork/Join框架的基本思想,即:
以递归的方式将可以并行的任务拆分成更小的子任务,然后将每个子任务的结果合并起来生成整体的最后结果
Fork/Join

理解了方法本身的意思以及流的工作方式,再结合到一起理解一下stream.reduce()方法,
即用Fork/Join的方式把一堆数据聚合成一个数据,因此可以画出reduce方法的运行草图

结合草图,要实现stream.reduce()方法,必须要告诉JDK:

  1. 你有什么需求数据要汇聚?(Stream已经提供了数据源,对应上面草图的A元素)

  2. 最后要汇聚成怎样的一个数据类型, 和提供初始值(对应reduce方法的参数一,对应上面草图的B元素, [从引用上来说:应该后面没有数字,因为都是一个引用B; 但是从内存地址上来说, B加上另外一个元素,就不是b了,比如:B是1,1+2就不是原来B的地址了])

  3. 如何将需求数据处理或转化成一个汇聚数据(对应reduce方法的参数二,对应上面草图的汇聚方式1, 累加器)

  4. 如何将多个汇聚数据进行合并(对应reduce方法的参数三,对应上面草图的汇聚方式2, 合路器)


两个参数和三个参数的区别:

reduce(初始值,累加器,合路器) //合路器只在多线程的时候使用.

那么第三个参数有什么用呢?第三个参数是两个相同的类型计算出一个相同的类型,而且都是最终返回类型。

第三个参数只有在并行计算即.stream().parallel()之后才会发生作用。

回想一下,如果不采用并行计算,reduce的计算方式肯定是单线程的,因为不可能直接计算出最终结果,而是每一个计算都依赖于上一个计算。

而并行计算的话,很显然与非并行计算的计算方式不同,如果还是按照原来reduce的方式进行计算,多线程也没有用,因为结果一步一步都互相依赖。

实际上,并行计算之后,计算方式不再是将结果依次与下一个元素计算,而是直接拿初始值与流中的每一个元素进行计算,这个过程被第二个参数控制。

用一个简单的例子来说明:

1
2
3
4
5
6
7
8
9
10
Stream<Integer> integerStream = Stream.of(3, 4, 5, 6, 7);
Stream<Integer> integerStream2 = Stream.of(3, 4, 5, 6, 7);

int result = integerStream2.reduce(10, (x, y) -> x + y);

int parellelResult = integerStream.parallel().reduce(10, (x, y) -> x + y, (x, y) -> (x * y));

System.out.println("单线程操作的结果是:"+result);

System.out.println("并行操作的结果是:"+parellelResult);

单线程操作的结果很明显,是10+3+4+5+6+7=35
并行操作呢,结果实际上是(10+3)*(10+4)(10+5)*(10+6)*(10+7)
第二个参数中的(x,y)实际上变成了初始值和每一个元素的运算过程,最后一个参数则表示将所有的结果相乘。


在使用三个参数的reduce的时候,reduce(初始值,累加器,合路器)
累加器一般有两个参数,(x,y)->method(Class param),
第一个参数x代表的是初始值
第二个参数y代表的是method的param


三个重载方法,有四种应用场合,两种计算逻辑。应用reduce的情况可以总结如下

  • 不需要初始值且无需改变返回类型,使用单参数的reduce方法,注意返回的是Optional对象
  • 需要初始值且无需改变返回类型,使用两参数的reduce方法,直接返回指定类型
  • 单线程对所有元素依次操作,需要返回其他类型,使用三参数的reduce方法,第三个参数随便传个lambda表达式即可,没有影响
  • 多线程同时操作所有元素然后进行联合,需要增加.parallel(),此时reduce的计算逻辑会发生变化。



测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import cn.hutool.core.lang.Console;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class ParallelStreamReduce {

public static void main(String[] args) {
// accumulator不写入list,不需要线程同步,初始值使用普通的list
List<Integer> list = new ArrayList<>();
List<Integer> reduceResult = IntStream.range(0, 100)
.parallel()
.boxed()
.reduce(list, (i, j) -> {
//不改变初始的i,而是返回一个新的i
ArrayList<Integer> newI = new ArrayList<>(i);
newI.add(j);
return newI;
}, (i, j) -> {
System.out.println(String.format("i==j: %s, thread name:%s", i == j, Thread.currentThread().getName()));
ArrayList<Integer> newI = new ArrayList<>(i);
newI.addAll(j);
return newI;
});
// System.out.println("reduce result size: " + reduceResult.size());
System.out.println("reduce result : " + reduceResult);

System.out.println("------------------------------------------------------------------------------");

Stream<Integer> integerStream = Stream.of(3, 4, 5, 6, 7);
Stream<Integer> integerStream2 = Stream.of(3, 4, 5, 6, 7);

int singleResult = integerStream2.reduce(10, (x, y) -> x + y);

int parallelResult = integerStream.parallel().reduce(10, (x, y) -> {
Console.log("x={},y={} \n", x, y);
return x + y;
}, (x, y) -> (x * y));

System.out.println("单线程操作的结果是:" + singleResult);

System.out.println("并行操作的结果是:" + parallelResult);
}
}





备份

reduce的重载秘密

关于Java Lambda表达式看这一篇就够了

java8中3个参数的reduce方法怎么理解?